﻿// Author:  Marco van Wieren <mailto:mvwieren@gmail.com>
// Year:    2012
// License: Microsoft Public License (Ms-PL)

using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Taxonomy;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebControls;
using System.Web.UI.WebControls;
using System.Web.UI;
using Microsoft.Office.DocumentManagement.MetadataNavigation;
using Mvw.SharePoint.Taxonomy.AutoTagging.Licenses;

namespace Mvw.SharePoint.Taxonomy.AutoTagging
{
    /// <summary>
    /// A static class packed with .net extension functions to support the AutoTagging application.
    /// </summary>
    public static class Ext
    {
        #region CORE EXTENSIONS

        /// <summary>
        /// An improved version of the c# Contains method as it uses IndexOf in combination with a string
        /// comparison object e.g. to search case insensitive.
        /// </summary>
        /// <param name="haystack">The string being searched.</param>
        /// <param name="needle">The string being searched for.</param>
        /// <param name="comp">Indication how the comparison of two strings is executed.</param>
        /// <returns>A boolean value indicating whether or not the needle was found in the haystack or not.</returns>
        public static bool Contains(this string haystack, string needle, StringComparison comp)
        {
            if (needle == null)
            {
                return false;
            }
            return haystack.IndexOf(needle, comp) >= 0;
        }

        public static Dictionary<string, string> GetDictionary(this string workspaceSettings)
        {
            var dict = new Dictionary<string, string>();
            var pairs = workspaceSettings.Split(';');
            foreach (var pair in pairs)
            {
                var splittedPair = pair.Split(':');
                if (splittedPair == null)
                {
                    continue;
                }
                if (splittedPair.Length == 2)
                {
                    dict.Add(splittedPair[0], splittedPair[1]);
                }
            }
            return dict;
        }

        public static string DictionaryToString(this Dictionary<string, string> dict)
        {
            var dictList = new List<string>();
            foreach (KeyValuePair<string, string> keyvalue in dict)
            {
                dictList.Add(keyvalue.Key + ":" + keyvalue.Value);
            }
            return string.Join(";", dictList.ToArray());
        }

        #endregion

        #region SHAREPOINT RELATED EXTENSIONS

        /// <summary>
        /// Method to return the parent web or if the current web is the root web the
        /// root web will be returned
        /// </summary>
        /// <param name="currWeb"></param>
        /// <returns></returns>
        public static SPWeb ParentOrRootWeb(this SPWeb currWeb)
        {
            SPWeb parentWeb = null;
            if (currWeb.ParentWeb != null)
            {
                parentWeb = currWeb.Site.OpenWeb(currWeb.ParentWeb.ID);
            }
            else
            {
                // This implies that the current web doesn't have a parent web and hence it's the root web
                // so parent web and current web are in fact the same
                parentWeb = currWeb.Site.OpenWeb(currWeb.Site.RootWeb.ID);
            }
            return parentWeb;
        }

        /// <summary>
        /// Simple method that returns login name and email of the first site collection administrator found for a site.
        /// </summary>
        /// <param name="site">The site for which the first site collection administrator's details should be returned.</param>
        /// <returns>A string[2] (which may contain empty strings if the search failed) with the site
        /// collection administrator's login name (1) and his email address (2).</returns>
        public static string[] SiteCollectionAdminDetails(this SPSite site)
        {
            string[] result = { string.Empty, string.Empty };
            foreach (SPUser user in site.RootWeb.AllUsers)
            {
                if (user.IsSiteAdmin)
                {
                    result[0] = user.LoginName;
                    result[1] = user.Email;
                }
            }
            return result;
        }

        /// <summary>
        /// Simple method that returns the first SPUser that is site collection administrator or null.
        /// </summary>
        /// <param name="site">The current site collection for which the first administrator will be returned.</param>
        /// <returns>The first SPUser that is site collection administrator for this site or null.</returns>
        public static SPUser FirstSiteCollectionAdmin(this SPSite site)
        {
            foreach (SPUser user in site.RootWeb.AllUsers)
            {
                if (user.IsSiteAdmin)
                {
                    return user;
                }
            }
            return null;
        }

        /// <summary>
        /// Simple Method that tries and sees if a relative url contains a known managed path.
        /// </summary>
        /// <param name="relativeUrl">A string representing a relative url that may or may not contain a managed path.</param>
        /// <param name="webApp">The web application queried for possible managed paths</param>
        /// <returns>The managed path without any forward slashes or null if not found.</returns>
        public static string GetManagedPath(string relativeUrl, SPWebApplication webApp)
        {
            if (relativeUrl == null || webApp == null)
            {
                return null;
            }

            for (var i = webApp.Prefixes.Count - 1; i >= 0; i--)
            {
                if (relativeUrl.Contains(webApp.Prefixes[i].Name, StringComparison.CurrentCultureIgnoreCase))
                {
                    return webApp.Prefixes[i].Name;
                }
            }
            return string.Empty;
        }

        /// <summary>
        /// This method will remove any last slash from a url (part).
        /// </summary>
        /// <param name="urlPart">A string representing a (part of) an url.</param>
        /// <returns>The url (part) parameter without any trailing slashes.</returns>
        public static string RemoveLastHash(this string urlPart)
        {
            if (urlPart.EndsWith("/"))
            {
                urlPart = urlPart.Substring(0, urlPart.Length - 1);
            }
            return urlPart;
        }

        /// <summary>
        /// Scan all sites below the current web application and return all site collection ids. Optionally site ids returned are
        /// only those of site collections that were previously created using this AutoTagging application.
        /// </summary>
        /// <param name="webApp">Current web application for which (all) site collection ids are returned.</param>
        /// <param name="mustHaveWorkspaceTermInfo">Boolean indicator whether or not to only return those site collections
        /// that were previously created using the AutoTagging application.</param>
        /// <returns></returns>
        public static List<Guid> GetSiteIdsForWebApplication(this SPWebApplication webApp, bool mustHaveWorkspaceTermInfo)
        {
            // TODO Cap the number of site collections to avoid performance issues
            var siteIds = new List<Guid>();
            for (int i = 0; i < webApp.Sites.Count; i++)
            {
                using (SPSite currSite = webApp.Sites[i])
                {
                    if (currSite == null)
                    {
                        continue;
                    }

                    if (mustHaveWorkspaceTermInfo)
                    {
                        // Check whether the site collection was created by our application
                        if (currSite.RootWeb.AllProperties["WorkspaceTermInfo"] != null)
                        {
                            siteIds.Add(currSite.ID);
                        }
                    }
                    else
                    {
                        siteIds.Add(currSite.ID);
                    }
                }
            }
            return siteIds;
        }

        /// <summary>
        /// A (very) simple way of showing the standard SharePoint status update.
        /// </summary>
        /// <param name="obj">The current layout page that will show the status update.</param>
        /// <param name="msg">A string containing the message shown.</param>
        public static void ShowMessage(this LayoutsPageBase obj, string type, string msg, string backgroundColor)
        {
            var statusUpdate = "function showStatusUpdate() { strStatusID = SP.UI.Status.addStatus('" + type + " : ', '<font color=\"#000000\">" + msg + "</font>', true); SP.UI.Status.setStatusPriColor(strStatusID, '" + backgroundColor + "'); }";
            //@"function showStatusUpdate() { strStatusID = SP.UI.Status.addStatus(""Information : "", ""<img src='/_Layouts/Images/STS_ListItem_43216.gif' align='absmiddle'> <font color='#AA0000'>We added " + listEntriesAdded + @" new entries to the <i>Workspaces</i> list.</font>"", true); SP.UI.Status.setStatusPriColor(strStatusID, ""yellow""); } "
            obj.Page.ClientScript.RegisterStartupScript(obj.GetType(), "SPMStatusUpdate", statusUpdate, true);
            obj.Page.ClientScript.RegisterStartupScript(obj.GetType(), "SPjs", "ExecuteOrDelayUntilScriptLoaded(showStatusUpdate, 'SP.js');", true);
        }

        /// <summary>
        /// Method to return all installed LCIDs for the local farm
        /// </summary>
        /// <returns>List<int> containing all installed LCIDs for the local farm</returns>
        public static List<int> AllAvailableLanguageInFarm()
        {
            SPFarm farm = SPFarm.Local;
            SPLanguageCollection installedLanguages = SPRegionalSettings.GlobalInstalledLanguages;
            List<int> lcids = new List<int>();
            foreach (SPLanguage installedLanguage in installedLanguages)
            {
                lcids.Add(installedLanguage.LCID);
            }
            return lcids;
        }

        /// <summary>
        /// A rather simplistic solution as the routine doesn't verify whether it is (still) possible to
        /// create site collections below the managed path. Assuming that this may change very quickly
        /// anyway such logic should be implemented by the calling routine instead.
        /// </summary>
        /// <param name="webApp"></param>
        /// <returns></returns>
        public static List<string> AllAvailableManagedPathForWebApp(SPWebApplication webApp)
        {
            List<string> manPaths = new List<string>();
            
            if (webApp == null)
            {
                return manPaths;
            }

            foreach (SPPrefix prefix in webApp.Prefixes)
            {
                manPaths.Add(prefix.Name);
            }
            return manPaths;
        }

        /// <summary>
        /// A method that will return (sorted by language) all available web templates in the form "lcid:ID" e.g. "1033:STS#1"
        /// </summary>
        /// <param name="site"></param>
        /// <param name="lcids"></param>
        /// <returns></returns>
        public static List<string> AllAvailableWebTemplatesByLanguage(this SPSite site, List<int> lcids)
        {
            List<string> webTemplatesByLanguage = new List<string>();
            
            if(lcids == null) 
            {
                return webTemplatesByLanguage;
            }

            foreach(int lcid in lcids) 
            {
                try
                {
                    SPWebTemplateCollection webTemplates = site.GetWebTemplates(Convert.ToUInt32(lcid));
                    foreach (SPWebTemplate template in webTemplates)
                    {
                        webTemplatesByLanguage.Add(lcid.ToString() + ":" + template.Name);
                    }
                }
                catch {}
            }
            return webTemplatesByLanguage;
        }

        #endregion

        #region TAXONOMY EXTENSIONS

        public static void ResetWorkspaceColumns(SPWeb currWeb, SPSite currSite)
        {
            // Update choices for the ManagedPath column of the Managed Workspace ContentType
            var manPathField = currSite.RootWeb.Fields.TryGetFieldByStaticName("WorkspaceManagedPath") as SPFieldChoice;
            if (manPathField == null)
            {
                throw new ChoiceFieldsUpdateException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorChoiceFieldsUpdate",
                   "SPM.Resources", currWeb.Language) + " manpath");
            }
            manPathField.Choices.Clear();
            foreach (string prefix in Ext.AllAvailableManagedPathForWebApp(currSite.WebApplication))
            {
                manPathField.Choices.Add(prefix);
            }
            // Managed Path can't be updated after the site has been created
            manPathField.ShowInEditForm = false;
            manPathField.Update(true);

            // Prepare choices and column for the Managed Workspace ContentType its languages
            var lcidField = currWeb.Fields.TryGetFieldByStaticName("WorkspaceLCID") as SPFieldChoice;
            if (lcidField == null)
            {
                throw new ChoiceFieldsUpdateException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorChoiceFieldsUpdate",
                   "SPM.Resources", currWeb.Language) + " language");
            }
            lcidField.Choices.Clear();

            // Prepare choices and column for the Managed Workspace ContentType its available web templates
            var webTemplateField = currWeb.Fields.TryGetFieldByStaticName("WorkspaceTemplate") as SPFieldChoice;
            if (webTemplateField == null)
            {
                throw new ChoiceFieldsUpdateException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorChoiceFieldsUpdate",
                   "SPM.Resources", currWeb.Language) + " template");
            }
            webTemplateField.Choices.Clear();

            // Now populate both language and web template column
            ArrayList unsortedLanguages = new ArrayList();
            foreach (SPLanguage lang in currWeb.RegionalSettings.InstalledLanguages)
            {
                unsortedLanguages.Add(lang.DisplayName + " [" + lang.LCID.ToString() + "]");
                ArrayList unsortedWebTemplates = new ArrayList();
                foreach (SPWebTemplate template in currSite.GetWebTemplates((uint)lang.LCID))
                {
                    unsortedWebTemplates.Add("(" + lang.DisplayName + ") " + template.Title + " [" + template.Name + "]");
                }
                unsortedWebTemplates.Sort();
                webTemplateField.Choices.AddRange(
                    ((System.Collections.IEnumerable)unsortedWebTemplates)
                      .Cast<object>()
                      .Select(x => x.ToString())
                      .ToArray());
            }
            unsortedLanguages.Sort();
            lcidField.Choices.AddRange(
                ((System.Collections.IEnumerable)unsortedLanguages)
                      .Cast<object>()
                      .Select(x => x.ToString())
                      .ToArray());

            // Set the current's web language as the default
            lcidField.DefaultValue = currWeb.Language.ToString();
            
            // Language can't be updated after the site has been created
            lcidField.ShowInEditForm = false;
            lcidField.Update(true);

            // Template can't be updated after the site has been created
            webTemplateField.ShowInEditForm = false;
            webTemplateField.Update(true);
        }

        /// <summary>
        /// Method to reset the Workspaces list, meaning that the content type the list is based on will be updated with all available
        /// taxonomy fields found in the site column group name "Managed Workspace". After the reset the Workspaces list is populated. 
        /// This means that new list entries are created for each workspace (optionally all site collections) below the current web application.
        /// </summary>
        /// <param name="workspacesList"></param>
        /// <param name="siteIds"></param>
        /// <returns></returns>
        // TODO The following method has too deep nesting
        public static int ResetAndPopulate(this SPList workspacesList, List<Guid> siteIds)
        {
            int listEntriesAdded = 0;
            if (workspacesList == null)
            {
                throw new WorkspacesListNotFoundException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorWorkspacesListNotFound",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [RAP0010]");
            }

            // Trial License -> This should be commented out for the retail version 
            if (!TrialLicense.verifyTrialStartDate(workspacesList.ParentWeb))
            {
                throw new TrialLicenseExpiredException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorTrialLicense",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [RAP0015]");
            }

            // First delete all current entries (if any) in the Workspaces list but ensure the ListItemDeleting event 
            // is not triggered (as this will subsequently delete all site collections)
            Ext.RunWithoutEventsFiring(delegate()
            {
                for (var i = workspacesList.Items.Count - 1; i >= 0; i--)
                {
                    workspacesList.Items[i].Delete();
                }
            });

            // "Managed Workspace" is the name of the content type for the Workspaces list and to
            // which any additional columns (added by a user) will be added
            if (workspacesList.ContentTypes["Managed Workspace"] == null)
            {
                throw new WorkspacesContentTypeNotFoundException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorWorkspaceContentTypeNotFound",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [RAP0020]");
            }

            // Get a reference to the current web application
            var webApp = workspacesList.ParentWeb.Site.WebApplication;

            // All available taxonomy fields found in the site column group "Inheritable Columns"
            var availableTaxonomyFields = workspacesList.ParentWeb.AvailableInheritableColumns();

            // Stop all list item events from firing or else for each entry the application will try 
            // (and fail) to create the existing site collection again
            Ext.RunWithoutEventsFiring(delegate()
            {
                for (int i = 0; i < siteIds.Count; i++)
                {
                    // Create a site collection instance
                    using (var siteBeingAdded = new SPSite(siteIds[i]))
                    {
                        // This is where we store any error that may occur
                        string errorMessage = string.Empty;

                        // Ensure the site object is not null
                        if (siteBeingAdded == null)
                        {
                            // Basically this error cannot happen as we just got a fresh list of site ids
                            // But when it happens it means that the site no longer exists or cannot be opened by the
                            // current user which is then by design and not an error so we continue.
                            continue;
                        }

                        // Get the details from the first site collection admin found
                        SPUser siteAdmin = siteBeingAdded.FirstSiteCollectionAdmin();
                        if (siteAdmin == null)
                        {
                            // Basically this error cannot happen because if there's a site collection then there will be an Administrator
                            throw new UnexpectedError(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorUnexpected",
                                "SPM.Resources", SPContext.Current.Web.Language) + " [RAP0040]");
                        }

                        SPListItem itemToAdd = workspacesList.Items.Add();

                        itemToAdd["Title"] = siteBeingAdded.RootWeb.Title;
                        itemToAdd["WorkspaceManagedPath"] = Ext.GetManagedPath(siteBeingAdded.RootWeb.ServerRelativeUrl, webApp);
                        itemToAdd["WorkspaceUrl"] = webApp.GetResponseUri(Microsoft.SharePoint.Administration.SPUrlZone.Default).ToString().RemoveLastHash() + siteBeingAdded.RootWeb.ServerRelativeUrl;
                        itemToAdd["WorkspaceAdminLogin"] = siteAdmin.ID;
                        itemToAdd["WorkspaceAdminEmail"] = siteBeingAdded.SiteCollectionAdminDetails()[1];

                        // A workaround to avoid using the Locale in the form of "en-US"
                        SPLanguage language = null;
                        foreach(SPLanguage lang in siteBeingAdded.RootWeb.RegionalSettings.InstalledLanguages) 
                        {
                            if (lang.LCID == siteBeingAdded.RootWeb.Locale.LCID)
                            {
                                language = lang;
                            }
                        }
                        if (language == null)
                        {
                            language = siteBeingAdded.RootWeb.RegionalSettings.ServerLanguage;
                        }

                        itemToAdd["WorkspaceLCID"] = language.DisplayName + " [" + language.LCID.ToString() + "]";

                        SPWebTemplate template = siteBeingAdded.RootWeb.GetAvailableWebTemplates((uint)siteBeingAdded.RootWeb.Locale.LCID)[siteBeingAdded.RootWeb.WebTemplateId];
                        itemToAdd["WorkspaceTemplate"] = "(" + language.DisplayName + ") " + template.Title + " [" + template.Name + "]";

                        if (siteBeingAdded.RootWeb.AllProperties["WorkspaceTermInfo"] == null)
                        {
                            itemToAdd.Update();
                            listEntriesAdded++;
                            continue;
                        }
                        
                        var workspaceTermInfoList = new WorkspaceTermInfoList(siteBeingAdded.RootWeb.AllProperties["WorkspaceTermInfo"].ToString());
                        if (!workspaceTermInfoList.IsValid)
                        {
                            itemToAdd["WorkspaceLatestError"] = SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                                "SPM.Resources", SPContext.Current.Web.Language) + " [RAP0050]";
                            itemToAdd.Update();
                            listEntriesAdded++;
                            continue;
                        }

                        foreach (var workspaceTermInfo in workspaceTermInfoList)
                        {
                            if (!workspaceTermInfo.IsValid)
                            {
                                itemToAdd["WorkspaceLatestError"] = SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                                    "SPM.Resources", SPContext.Current.Web.Language) + " [RAP0060]";
                                continue;
                            }

                            foreach (var taxonomyInheritableField in availableTaxonomyFields)
                            {
                                // If a WorkspaceTermInfo object is found then try and set corresponding taxonomy field(s)
                                // for the current list item to the value as it was stored in the WorkspaceTermInfo
                                // !!!!! IMPORTANT !!!!!
                                // The application will NOT update any site collection in case new taxonomy fields were added.
                                // Updating meta data involves manual actions such as selecting the correct term for each
                                // workspace. At this stage we merely restore the status quo.
                                if (string.Equals(taxonomyInheritableField.StaticName, workspaceTermInfo.TaxFieldStaticName, StringComparison.InvariantCultureIgnoreCase))
                                {
                                    // A field named in the site collection's Workspace Term Info is no(t) (longer) present in the Worksapces List -> Update manually later
                                    if (itemToAdd.Fields.TryGetFieldByStaticName(workspaceTermInfo.TaxFieldStaticName) == null) 
                                    {
                                        continue;
                                    }
                                    // Make sure the field is indeed a managed meta data field
                                    if (!(itemToAdd.Fields.TryGetFieldByStaticName(workspaceTermInfo.TaxFieldStaticName) is TaxonomyField))
                                    {
                                        continue;
                                    }
                                    // Make sure we catch a possible error here so that it doesn't propagate to the surface and 
                                    // stops this routine from executing - Instead the user informed via the workspacelatesterror field
                                    try
                                    {
                                        (itemToAdd.Fields.TryGetFieldByStaticName(workspaceTermInfo.TaxFieldStaticName) as TaxonomyField).EnsureTaxonomyValue(workspacesList.ParentWeb, workspaceTermInfo, itemToAdd, false);
                                    }
                                    catch(Exception ex)
                                    {
                                        itemToAdd["WorkspaceLatestError"] = ex.Message + " [RAP0060]";
                                        continue;
                                    }
                                    
                                }
                            }
                        }
                        itemToAdd.Update();
                        listEntriesAdded++;
                    }
                }
            });

            // Now try to enable meta data navigation (filtering) for the available taxonomy fields
            try
            {
                MetadataNavigationSettings listNavSettings = MetadataNavigationSettings.GetMetadataNavigationSettings(workspacesList);
                listNavSettings.ClearConfiguredKeyFilters();
                listNavSettings.ClearConfiguredHierarchies();

                foreach(var taxonomyField in availableTaxonomyFields)
                {
                    MetadataNavigationKeyFilter mdnKeyFilter = new MetadataNavigationKeyFilter(taxonomyField);
                    listNavSettings.AddConfiguredKeyFilter(mdnKeyFilter); 
                }
                
                MetadataNavigationSettings.SetMetadataNavigationSettings(workspacesList, listNavSettings, true);
                // workspacesList.Update();
            }
            catch { }

            return listEntriesAdded;
        }

        /// <summary>
        /// This method looks up all TaxonomyFields in a web that are found in the site columns group
        /// "Inheritable Columns". These are the columns that will be added to the
        /// "Managed Workspace" content type which is the base content type for the Workspaces
        /// list via which this application creates the Workspaces. Adding or deleting columns to the "Inheritable Columns"
        /// group and resetting the Workspaces list (by executing the "Reset List" custom action
        /// in the ribbon of Workspaces list) will result to a changes in the fields that will be inherited.
        /// </summary>
        /// <param name="web">The current web for which the site columns will be searched.</param>
        /// <returns>A list of TaxonomyFields found in the group "Inheritable Columns".</returns>
        public static List<TaxonomyField> AvailableInheritableColumns(this SPWeb web)
        {
            var availableTaxonomyFields = new List<TaxonomyField>();
            for (var i = 0; i < web.Fields.Count; i++)
            {
                if (string.Equals(web.Fields[i].Group, "Inheritable Columns", StringComparison.CurrentCultureIgnoreCase))
                {
                    // We only want the meta data columns
                    if (web.Fields[i] is TaxonomyField)
                    {
                        availableTaxonomyFields.Add(web.Fields[i] as TaxonomyField);
                    }
                }
            }
            return availableTaxonomyFields;
        }

        /// <summary>
        /// This method will ensure the availability of a TaxonomyField that corresponds to the information
        /// stored in the WorkspaceTermInfo object supplied.
        /// </summary>
        /// <param name="web">The current web for which the availability of the TaxonomyField is to be ensured.</param>
        /// <param name="workspaceTermInfo">The taxonomy information for the TaxonomyField being verified.</param>
        /// <param name="createIfNotFound">A boolean indicator expressing whether a new TaxonomyFields should be
        /// created if none was found.</param>
        /// <returns>A boolean value indicating that a specific TaxonomyFiels is available in a web.</returns>
        public static void EnsureTaxonomyField(this SPWeb web, WorkspaceTermInfo workspaceTermInfo, bool createIfNotFound)
        {
            if (web == null)
            {
                throw new NullReferenceException("[ETF0010]");
            }

            var session = new TaxonomySession(web.Site); // TODO Put this plumbing in its own class
            if (session.TermStores.Count <= 0)
            {
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETF0020]");
            }

            var termStore = session.TermStores[workspaceTermInfo.TermStoreName];
            if (termStore == null)
            {
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETF0030]");
            }
            var termSet = termStore.GetTermSet(new Guid(workspaceTermInfo.TermSetId));
            if (termSet == null)
            {
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETF0040]");
            }

            // Does the field exists?
            TaxonomyField taxField = web.Fields.TryGetFieldByStaticName(workspaceTermInfo.TaxFieldStaticName) as TaxonomyField;
            if (taxField == null && createIfNotFound)
            {
                try
                {
                    // TODO Provide a more gentle name by replacing _x0020_ for the DisplayName
                    taxField = web.Fields.CreateNewField("TaxonomyFieldType", workspaceTermInfo.TaxFieldStaticName.Replace("_x0020_", " ")) as TaxonomyField;
                    taxField.StaticName = workspaceTermInfo.TaxFieldStaticName;
                    taxField.SspId = termSet.TermStore.Id;
                    taxField.TermSetId = termSet.Id;
                    taxField.TargetTemplate = string.Empty;
                    taxField.AllowMultipleValues = workspaceTermInfo.AllowMultipleValues;
                    taxField.CreateValuesInEditForm = true;
                    taxField.Open = true;
                    taxField.AnchorId = Guid.Empty;
                    taxField.Group = "Inherited Columns";
                    web.Fields.Add(taxField);
                    web.Update();
                    taxField = web.Fields.TryGetFieldByStaticName(workspaceTermInfo.TaxFieldStaticName) as TaxonomyField;
                }
                catch (Exception ex)
                {
                    throw new EnsureTaxonomyFieldException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorEnsureTaxonomyField",
                        "SPM.Resources", SPContext.Current.Web.Language) + " [ETF0050]", ex);
                }
                
            }

            taxField.EnsureTaxonomyValue(web, workspaceTermInfo, null, true);
        }

        /// <summary>
        /// This method does a number of things. First it ensures that the TaxonomyField is properly registered in a site's local
        /// "cache" (a hidden that each web has that keeps track of metadata available centrally in the managed meta data service
        /// used locally). It then tries to set the field's value either to multiple values or to a single value. Optionally it can
        /// set the given value as the default value for the TaxonomyField. If a list item was supplied, this method can additionally 
        /// pass the value on the list item.
        /// </summary>
        /// <param name="taxField">The TaxonomyField for which a value is to be ensured.</param>
        /// <param name="web">The parent web of the TaxonomyField.</param>
        /// <param name="workspaceTermInfo">The taxonomy information for the TaxonomyField being verified.</param>
        /// <param name="listItem">Optionally: A list item that should receive a copy of the value of the TaxonomyField.</param>
        /// <param name="setAsDefaultValue">Optionally: If yes, the TaxonomyField's default value will be set accordingly.</param>
        /// <returns></returns>
        // TODO The term's wssId is currently not set for terms that allow for multiple values and this needs be investigated
        public static void EnsureTaxonomyValue(this TaxonomyField taxField, SPWeb web, WorkspaceTermInfo workspaceTermInfo, SPListItem listItem, bool setAsDefaultValue)
        {
            if (web == null)
            {
                throw new NullReferenceException("[ETV0010]");
            }

            if (workspaceTermInfo == null)
            {
                throw new WorkspaceTermInfoNotFoundException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorWorkspaceTermInfoNotFound",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETV0020]");
            }

            // Nothing to do here
            if (workspaceTermInfo.TermIds.Count == 0)
            {
                return;
            }

            var session = new TaxonomySession(web.Site);
            if (session.TermStores.Count <= 0)
            {
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETV0030]");
            }

            var termStore = session.TermStores[workspaceTermInfo.TermStoreName];
            if (termStore == null)
            {
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETV0040]");
            }

            var termSet = termStore.GetTermSet(new Guid(workspaceTermInfo.TermSetId));
            if (termSet == null)
            {
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETV0050]");
            }

            // An unexpected incompatibility -> Did a user changed something manually?
            if (taxField.AllowMultipleValues != workspaceTermInfo.AllowMultipleValues)
            {
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ETV0060]");
            }
 
            if (workspaceTermInfo.AllowMultipleValues)
            {
                var taxFieldValues = new TaxonomyFieldValueCollection(taxField);
                var termLabelGuidPairs = new List<string>();
                foreach (var termGuid in workspaceTermInfo.TermIds)
                {
                    var termLabelGuidPair = TermLabelGuidPair(termSet, termGuid);
                    
                    if (string.IsNullOrEmpty(termLabelGuidPair))
                    {
                        //throw new EnsureTaxonomyFieldValueException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorEnsureTaxonomyFieldValue",
                        //    "SPM.Resources", SPContext.Current.Web.Language) + " : [" + workspaceTermInfo.ToString() + "]");
                        // This will be the case when terms have been deleted (not recommended)
                        return;
                    }

                    termLabelGuidPairs.Add(termLabelGuidPair);
                }
                taxFieldValues.PopulateFromLabelGuidPairs(string.Join(TaxonomyField.TaxonomyMultipleTermDelimiter.ToString(), termLabelGuidPairs.ToArray()));
                
                // This (should?) registers the term in the hidden list
                string valueWithId = taxField.GetValidatedString(taxFieldValues);

                // Default value for a field in a list or at site level
                if (setAsDefaultValue)
                {
                    taxField.DefaultValue = valueWithId;
                }

                // List item e.g. an entry in the Workspaces List
                if (listItem != null)
                {
                    taxField.SetFieldValue(listItem, taxFieldValues);
                }
            }
            else
            {
                var term = termSet.GetTerm(new Guid(workspaceTermInfo.TermIds[0]));
                if (term == null)
                {
                    // This will be the case when terms have been deleted (not recommended)
                    return;
                }
                var taxFieldValue = new TaxonomyFieldValue(taxField);
                taxFieldValue.TermGuid = term.Id.ToString();
                taxFieldValue.Label = term.GetPath();
                var valueWithId = taxField.GetValidatedString(taxFieldValue); // Register term with local site
                var wssIsds = TaxonomyField.GetWssIdsOfTerm(web.Site, termStore.Id, termSet.Id, term.Id, false, 1);
                if (wssIsds.Length < 1)
                {
                    throw new EnsureTaxonomyFieldValueException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorEnsureTaxonomyFieldValue",
                        "SPM.Resources", SPContext.Current.Web.Language) + " [ETV0070]");
                }

                // Verify that the term been successfully registered in the local site
                if (wssIsds[0] == -1)
                {
                    throw new EnsureTaxonomyFieldValueException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorEnsureTaxonomyFieldValue",
                        "SPM.Resources", SPContext.Current.Web.Language) + " [ETV0080]");
                }
                taxFieldValue.WssId = wssIsds[0];

                // Default value for a field in a list or at site level
                if (setAsDefaultValue)
                {
                    taxField.DefaultValue = taxFieldValue.ValidatedString;
                }

                // List item e.g. an entry in the Workspaces List
                if(listItem != null)
                {
                    taxField.SetFieldValue(listItem, taxFieldValue);
                }
            }
            taxField.Update();
        }

        /// <summary>
        /// Simple helper method to combine a label and a guid of a term to string.
        /// </summary>
        /// <param name="termSet">TermSet for the current term.</param>
        /// <param name="termGuid">Current term's guid.</param>
        /// <returns></returns>
        public static string TermLabelGuidPair(TermSet termSet, string termGuid)
        {
            if (termGuid == null)
            {
                return string.Empty;
            }

            if (termGuid.Length == 0)
            {
                return string.Empty;
            }

            Term term = null;
            try
            {
                term = termSet.GetTerm(new Guid(termGuid));
            }
            catch { }
            
            if (term == null)
            {
                return string.Empty;
            }

            return string.Concat(term.GetDefaultLabel(1033), TaxonomyField.TaxonomyGuidLabelDelimiter, term.Id);
        }

        #endregion

        #region HANDLE EVENTS FIRING

        // Some plumbing to temporarily disable events for a list
        public delegate void RunWithoutEventsFiringDelegate();

        public static void RunWithoutEventsFiring(RunWithoutEventsFiringDelegate codeBlock)
        {
            HandleEventsFiringEventReceiver eventReceiver = new HandleEventsFiringEventReceiver();
            try
            {
                eventReceiver.DisableEvents();
                codeBlock();
            }
            finally
            {
                eventReceiver.EnableEvents();
            }
        }

        #endregion

        #region WORKSPACE EVENTS METHODS

        /// <summary>
        /// Simple method that tries to enable SharePoint's TaxonomyFieldAdded feature as well as 
        /// our own Workspace feature that provides the infrastructure for local meta data inheritance
        /// when in or below the initial root web new list or webs are created 
        /// </summary>
        /// <param name="currWeb"></param>
        /// <param name="taxFieldAddedOnly"></param>
        public static void EnsureRequiredFeatures(this SPWeb currWeb, bool taxFieldAdded, bool autoTaggingWorkspaceInfrastructure, bool metadataNavigation)
        {
            // Activate SharePoint's taxonomy infrastructure
            if (taxFieldAdded)
            {
                if(currWeb.Site.Features[new Guid("73EF14B1-13A9-416b-A9B5-ECECA2B0604C")] == null) 
                {
                    try
                    {
                        currWeb.Site.Features.Add(new Guid("73EF14B1-13A9-416b-A9B5-ECECA2B0604C"));
                    }
                    catch { }
                }
            }

            // Activate SharePoint's meta data navigation
            if (metadataNavigation)
            {
                if (currWeb.Features[new Guid("7201D6A4-A5D3-49A1-8C19-19C4BAC6E668")] == null) 
                {
                    try
                    {
                        currWeb.Features.Add(new Guid("7201D6A4-A5D3-49A1-8C19-19C4BAC6E668"));
                    }
                    catch { }
                }
            }

            // Now activate the inheritance infrastructure
            if (autoTaggingWorkspaceInfrastructure)
            {
                if (currWeb.Features[new Guid("d4e3910e-7ea4-4a55-9e7e-f775a9a68f7e")] == null)
                {
                    try
                    {
                        currWeb.Features.Add(new Guid("d4e3910e-7ea4-4a55-9e7e-f775a9a68f7e"));
                    }
                    catch { }
                }
            }
        }
       

        /// <summary>
        /// This method will iterate through all lists within the current web and add to each list 
        /// the inherited TaxonomyFields.
        /// </summary>
        /// <param name="currWeb">The current web for which all lists will be verified.</param>
        public static void UpdateExistingLists(this SPWeb currWeb, bool updateListItems)
        {
            for (var i = 0; i < currWeb.Lists.Count; i++)
            {
                // TODO This algorithm can definately be improved and be more accurate
                if (currWeb.Lists[i].Hidden || currWeb.Lists[i].RootFolder.ServerRelativeUrl.Contains(@"_catalogs"))
                {
                    continue;
                }

                currWeb.Lists[i].EnsureInheritedColumns();

                if (updateListItems)
                {
                    currWeb.Lists[i].UpdateListItems();
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="list"></param>
        public static void UpdateListItems(this SPList currList)
        {
            using (var web = currList.ParentWeb.Site.OpenWeb(currList.ParentWeb.ID))
            {
                if (web.AllProperties["WorkspaceTermInfo"] == null)
                {
                    throw new WorkspaceTermInfoNotFoundException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorWorkspaceTermInfoNotFound",
                        "SPM.Resources", SPContext.Current.Web.Language) + " [ULI0010]");
                }

                var workspaceTermInfoList = new WorkspaceTermInfoList(web.AllProperties["WorkspaceTermInfo"].ToString());
                if (!workspaceTermInfoList.IsValid)
                {
                    throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                        "SPM.Resources", SPContext.Current.Web.Language) + " [ULI0020]");
                }

                // Discussion boards are a bit strange :)
                if (currList.BaseTemplate == SPListTemplateType.DiscussionBoard)
                {
                    foreach (SPListItem folder in currList.Folders)
                    {
                        // Update the main topic
                        UpdateListItem(web, currList, folder, workspaceTermInfoList);
                        foreach (SPListItem item in folder.ListItems)
                        {
                            // And its replies ??
                            UpdateListItem(web, currList, item, workspaceTermInfoList);
                        }
                    }
                }
                else
                {
                    foreach (SPListItem item in currList.Items)
                    {
                        UpdateListItem(web, currList, item, workspaceTermInfoList);
                    }
                }
                
            }
        }

        private static void UpdateListItem(SPWeb web, SPList currList, SPListItem item, WorkspaceTermInfoList workspaceTermInfoList)
        {
            foreach (var workspaceTermInfo in workspaceTermInfoList)
            {
                if (!workspaceTermInfo.IsValid)
                {
                    throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                        "SPM.Resources", SPContext.Current.Web.Language) + " [ULI0200]");
                }

                var taxonomyField = item.Fields.TryGetFieldByStaticName(workspaceTermInfo.TaxFieldStaticName) as TaxonomyField;
                if (taxonomyField == null)
                {
                    throw new ConfigurationNotInSyncException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationNotInSync",
                        "SPM.Resources", SPContext.Current.Web.Language) + " [ULI0210]");
                }

                try
                {
                    // TODO What is with a picture library??
                    var isDocument = false;
                    SPDocumentLibrary currLib = null;
                    if (currList is SPDocumentLibrary)
                    {
                        currLib = currList as SPDocumentLibrary;
                        if (currLib.EnableVersioning)
                        {
                            if (item.File.RequiresCheckout)
                            {
                                if (item.File.CheckOutType != SPFile.SPCheckOutType.None)
                                {
                                    item.File.CheckIn("$Resources:SPM.Resources, Ext_ForcedCheckIn");
                                }
                                item.File.CheckOut();
                                isDocument = true;
                            }
                        }
                    }

                    taxonomyField.EnsureTaxonomyValue(web, workspaceTermInfo, item, false);

                    if (isDocument)
                    {
                        item.File.CheckIn("$Resources:SPM.Resources, Ext_ForcedCheckInAfterUpdate");
                        if (currLib.EnableMinorVersions)
                        {
                            item.File.Publish("$Resources:SPM.Resources, Ext_ForcedPublishAfterUpdate");
                        }
                    }

                    item.Update();
                }
                catch // TODO Review whether we need to break or to continue
                {
                    continue;
                }
            }
        }

        /// <summary>
        /// This method first checks to see whether all columns found in the site column group "Inherited Columns"
        /// are already added to the list and will added if this is not the case. Be aware that
        /// this method will not delete any column that is no longer present. Once the columns are added the method will
        /// try and ensure their value using the information found in the WorkspaceTermInfo object stored in the list's parent web's
        /// property bag i.e. the default value is no longer used (was in previous versions).
        /// </summary>
        /// <param name="currList">The current list for which </param>
        public static void EnsureInheritedColumns(this SPList currList)
        {
            if (currList.ParentWeb.AllProperties["WorkspaceTermInfo"] == null)
            {
                throw new WorkspaceTermInfoNotFoundException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorWorkspaceTermInfoNotFound",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [EIC0010]");
            }

            for (var i = 0; i < currList.ParentWeb.Site.RootWeb.Fields.Count; i++)
            {
                if (string.Equals(currList.ParentWeb.Site.RootWeb.Fields[i].Group, "Inherited Columns", StringComparison.CurrentCultureIgnoreCase))
                {
                    // First add the taxonomy columns if not already added
                    if (currList.Fields.TryGetFieldByStaticName(currList.ParentWeb.Site.RootWeb.Fields[i].StaticName) == null) // Either add the field
                    {
                        currList.Fields.Add(currList.ParentWeb.Site.RootWeb.Fields[i]);
                    }

                    // Then retrieve it again
                    var taxField = currList.Fields.TryGetFieldByStaticName(currList.ParentWeb.Site.RootWeb.Fields[i].StaticName) as TaxonomyField;
                    if (taxField == null)
                    {
                        // We would not expect to not be able to retrieve something we just added
                        throw new UnexpectedError(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorUnexpected",
                            "SPM.Resources", SPContext.Current.Web.Language) + " [EIC0020]");
                    }

                    // Get the WorkspaceInfo
                    WorkspaceTermInfoList workspaceTermInfoList = new WorkspaceTermInfoList(currList.ParentWeb.AllProperties["WorkspaceTermInfo"].ToString());
                    if (!workspaceTermInfoList.IsValid)
                    {
                        throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                            "SPM.Resources", SPContext.Current.Web.Language) + " [EIC0030]");
                    }

                    // And ensure its value
                    // TODO Implement a WorkspaceTermInfoList search function
                    foreach (var workspaceTermInfo in workspaceTermInfoList)
                    {
                        if (!workspaceTermInfo.IsValid)
                        {
                            throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                                "SPM.Resources", SPContext.Current.Web.Language) + " [EIC0040]");
                        }
                        if (string.Equals(workspaceTermInfo.TaxFieldStaticName, taxField.StaticName, StringComparison.InvariantCultureIgnoreCase))
                        {
                            taxField.EnsureTaxonomyValue(currList.ParentWeb, workspaceTermInfo, null, true);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// This method updates the WorkspaceTermInfo in the current web's property bag with information
        /// found in it's parent web, ensure the TaxonomyFields and value(s) and then updates all lists 
        /// for the current web.
        /// </summary>
        /// <param name="currWeb">The current web being examined.</param>
        public static void InheritWorkspaceTermInfo(this SPWeb currWeb, bool updateListItems)
        {
            // Check whether workspace term info was inherited
            var parentWeb = currWeb.ParentOrRootWeb();

            // If the parent web doesn't have WorkspaceTermInfo then there's nothing to inherit
            if (parentWeb.AllProperties["WorkspaceTermInfo"] == null)
            {
                parentWeb.Dispose();
                return;
            }

            // If there is no configuration data then there is nothing to inherit, but if there is, it should be valid!
            var workspaceTermInfoList = new WorkspaceTermInfoList(parentWeb.AllProperties["WorkspaceTermInfo"].ToString());
            if (!workspaceTermInfoList.IsValid)
            {
                parentWeb.Dispose();
                throw new ConfigurationDataInvalidException(SPUtility.GetLocalizedString("$Resources:GLOBAL_ErrorConfigurationDataInvalid",
                    "SPM.Resources", SPContext.Current.Web.Language) + " [ITI0010]");
            }
            parentWeb.Dispose();

            currWeb.UpdateCurrentWeb(workspaceTermInfoList, updateListItems);
        }

        /// <summary>
        /// Method for recursively updating all webs below a current. Each time the current web
        /// will be updated with the information found in its parent or in case of the root web
        /// with the root web's own WorkspaceTermInfo.
        /// The initial argument currWeb's WorkspaceTermInfoInherit is not checked. This should 
        /// be handled by the caller. Subsequent nodes (and hence all site below a node) are
        /// checked however.
        /// </summary>
        /// <param name="currWeb">The current web.</param>
        public static void InheritWorkspaceTermInfoRecursively(this SPWeb currWeb, bool updateListItems)
        {
            currWeb.InheritWorkspaceTermInfo(updateListItems);
            for (var i = 0; i < currWeb.Webs.Count; i++)
            {
                using (var web = currWeb.Site.OpenWeb(currWeb.Webs[i].ID))
                {
                    try
                    {
                        if (!bool.Parse(web.AllProperties["WorkspaceTermInfoInherit"].ToString()))
                        {
                            web.Dispose();
                            continue;
                        }
                    }
                    catch { }

                    web.InheritWorkspaceTermInfoRecursively(updateListItems);
                    web.Dispose();
                }
            }
        }

        /// <summary>
        /// Helper method for the actual implementation of ensuring the taxonomy fields for
        /// a given WorkspaceTermInfoList.
        /// </summary>
        /// <param name="currWeb"></param>
        /// <param name="workspaceTermInfoList"></param>
        public static void UpdateCurrentWeb(this SPWeb currWeb, WorkspaceTermInfoList workspaceTermInfoList, bool updateListItems)
        {
            // For each WorkspaceTermInfo ensure that the TaxonomyField is available in the current web
            foreach (var workspaceTermInfo in workspaceTermInfoList)
            {
                if (!workspaceTermInfo.IsValid)
                {
                    continue;
                }
                currWeb.Site.RootWeb.EnsureTaxonomyField(workspaceTermInfo, true);
            }

            // And pass the configuration data on the current sub site
            currWeb.AllProperties["WorkspaceTermInfo"] = workspaceTermInfoList.ToString();
            currWeb.Update();

            // Finally update the existing lists
            currWeb.UpdateExistingLists(updateListItems);
        }

        #endregion
    }

}
